// Copyright 2012 Google Inc. All Rights Reserved.

package com.google.appengine.preferencessync.internal;

import com.google.appengine.preferencessync.adapter.SyncableSharedPreferencesAdapter;
import com.google.appengine.preferencessync.internal.InternalSyncException.ExceptionType;

import android.accounts.Account;
import android.accounts.AccountManager;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.util.Log;

import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.ObjectWriter;
import org.codehaus.jackson.type.TypeReference;

import java.util.ArrayList;

/**
 * Class for maintaining the sync configuration.
 * TODO(chintanraoh). Each public function call should be atomic.
 */
public class Config {

  private static final String RESERVED_PREFERENCES_NAME = "__SyncableSharedPreferences_";

  private static final String SYNC_CONFIG_NAME = RESERVED_PREFERENCES_NAME + "_Metadata_";

  public static final String SERVER_COPY_PREFIX = RESERVED_PREFERENCES_NAME + "_ServerCopy_";

  private static final String USERNAME_NOT_SET = "Username not set";

  private static final String USERNAME_DOES_NOT_EXISTS = "Google account %s does not exists";

  private static final String PAGING_SIZE_LESS_THAN_ZERO = "Paging size less than zero";

  private static final int PAGE_SIZE = 10000; // Random for now we have decide what this.

  private static final String ACCOUNT_TYPE = "com.google";

  // default object for this class.
  private static Config defaultConfig;

  /**
   * Specifies the merge conflict resolution type.
   */
  public enum MergeResolutionType {
    SERVER_WINS(PropertyKey.SERVER_WINS_PREFERENCES),
    CLIENT_WINS(PropertyKey.CLIENT_WINS_PREFERENCES),
    CUSTOM_WINS(PropertyKey.CUSTOM_WINS_PREFERENCES);

    // Preferences property associated with the strategy.
    private final PropertyKey propertyKey;

    /**
     * @param key {@link SharedPreferences} property key for storing list of preferences
     * for the strategy
     */
    private MergeResolutionType(PropertyKey key) {
      this.propertyKey = key;
    }

    /**
     * @return Return the property key for resolution type.
     */
    private PropertyKey getPropertyKey() {
      return propertyKey;
    }
  }

  /**
   * Property keys for properties stored in configuration represented by the
   * {@link SharedPreferences}.
   */
  private enum PropertyKey {
    USERNAME("USERNAME"),
    ROOT_URL("ROOT_URL"),
    CLIENT_ID("CLIENT_ID"),
    PAGING_SIZE("PAGING_SIZE"),

    PREFERENCES_TO_SYNC("PREFERENCES_TO_SYNC"),

    /**
     *  {@link SharedPreferences} property key for storing list of
     *  {@link SharedPreferences} names for client wins strategy.
     */
    CLIENT_WINS_PREFERENCES("CLIENT_WINS_PREFERENCES"),

    /**
     *  {@link SharedPreferences} property key for storing list of
     *  {@link SharedPreferences} names for server wins strategy.
     */
    SERVER_WINS_PREFERENCES("SERVER_WINS_PREFERENCES"),

    /**
     *  {@link SharedPreferences} property key for storing list of
     *  {@link SharedPreferences} names for custom wins strategy.
     */
    CUSTOM_WINS_PREFERENCES("CUSTOM_WINS_PREFERENCES"),

    /**
     * {@link SharedPreferences} property key for storing the gcm registration
     * id.
     */
    GCM_REGISTRATION_ID("GCM_REGISTRATION_ID"),

    /**
     * {@link SharedPreferences} property key for device id sent.
     */
    GCM_ID_SENT("GCM_ID_SENT");

    private final String propertyKey;

    private PropertyKey(String key) {
      this.propertyKey = key;
    }

    private String getPropertyKey() {
      return propertyKey;
    }
  }

  // SharedPreferences where all config is stored.
  private final SharedPreferences sharedPreferences;
  private final Context context;

  /**
   * Constructor.
   *
   * @param sharedPreferences Shared preferences file to use to store the config.
   */
  public Config(Context context, SharedPreferences sharedPreferences) {
    this.sharedPreferences = sharedPreferences;
    this.context = context;
  }

  /**
   * @return The client id of the android client. -1 if the client id is not present.
   */
  public long getClientId() {
    return sharedPreferences.getLong(PropertyKey.CLIENT_ID.getPropertyKey(), -1);
  }

  /**
   * @param clientId The client id to set in the config.
   */
  public void setClientId(long clientId) {
    Editor editor = sharedPreferences.edit();
    editor.putLong(PropertyKey.CLIENT_ID.getPropertyKey(), clientId);
    editor.commit();
  }

  /**
   * @return The username set in the config.
   */
  public String getUsername() {
    return sharedPreferences.getString(PropertyKey.USERNAME.getPropertyKey(), null);
  }

  /**
   * Set the username in the Config. If the username changes then it clears all sync metadata
   * associated with all preferences. If the username is not null then it checks whether the
   * account actually exists.
   */
  public void setUsername(String username) {
    String oldUserName = sharedPreferences.getString(PropertyKey.USERNAME.getPropertyKey(), null);

    if (username == null || !checkIfAccountExists(username)) {
      throw new IllegalArgumentException(String.format(USERNAME_DOES_NOT_EXISTS, username));
    }

    // If the username has changed then we have reset sync. Hence we clear metadata.
    if (!username.equals(oldUserName)) {
      try {
        InternalSyncUtils.clearMetadata(context);
      } catch (InternalSyncException e) {
        SyncClientLog.logError(this.getClass(), e);
      }
    }

    Editor editor = sharedPreferences.edit();
    editor.putString(PropertyKey.USERNAME.getPropertyKey(), username);
    editor.commit();
  }

  /**
   * Get the server root url.
   *
   * @return The root url.
   */
  public String getRootUrl() {
    return sharedPreferences.getString(PropertyKey.ROOT_URL.getPropertyKey(), null);
  }

  /**
   * @param url The root url of the server.
   */
  public void setRootUrl(String url) {
    Editor editor = sharedPreferences.edit();
    editor.putString(PropertyKey.ROOT_URL.getPropertyKey(), url);
    editor.commit();
  }

  /**
   * Add a new {@link SharedPreferences} which needs to be synced.
   *
   * @param preferences the {@link SharedPreferences} name.
   * @param type the merge resolution type to use for the {@link SharedPreferences}
   */
  public void addPreferencesToSync(String preferences, MergeResolutionType type) {
    try {
      addToSet(PropertyKey.PREFERENCES_TO_SYNC, preferences);
      setMergeConflictType(preferences, type);
    } catch (InternalSyncException e) {
      SyncClientLog.logError(this.getClass(), e);
    }
  }

  /**
   * Remove {@link SharedPreferences} from the list of preferences.
   *
   * @param preferences
   */
  public void removePreferencesToSync(String preferences) {
    try {
      removeFromSet(PropertyKey.PREFERENCES_TO_SYNC, preferences);
      // Remove from all resolution types.
      for (MergeResolutionType type : MergeResolutionType.values()) {
        removeFromSet(type.getPropertyKey(), preferences);
      }
    } catch (InternalSyncException e) {
      SyncClientLog.logError(this.getClass(), e);
    }
  }

  /**
   * @return The list of {@link SharedPreferences} which need to be synced.
   */
  public ArrayList<String> getAllPreferencesToSync() throws InternalSyncException {
    return getArrayList(PropertyKey.PREFERENCES_TO_SYNC);
  }

  /**
   * Set merge conflict type for the preferences.
   */
  private void setMergeConflictType(String preferences, MergeResolutionType type)
      throws InternalSyncException {
    // Add preferences to the type.
    addToSet(type.getPropertyKey(), preferences);

    // Remove preferences from all other type.
    for (MergeResolutionType otherType : MergeResolutionType.values()) {
      if (otherType.equals(type)) {
        continue;
      }
      removeFromSet(otherType.getPropertyKey(), preferences);
    }
  }

  /**
   * @param preferences
   * @return The resolution type for the preferences.
   */
  public MergeResolutionType getResolutionType(String preferences) throws InternalSyncException {
    for (MergeResolutionType type : MergeResolutionType.values()) {
      if (getArrayList(type.getPropertyKey()).contains(preferences)) {
        return type;
      }
    }
    return null;
  }

  /**
   * @param pagingSize pageSize for paging the data to be sent to be sent to the server.
   */
  public void setPageSize(int pagingSize) {
    Editor editor = sharedPreferences.edit();
    editor.putInt(PropertyKey.PAGING_SIZE.getPropertyKey(), pagingSize);
    editor.commit();
  }

  /**
   * @return pageSize for paging the data to be sent to be sent to the server.
   */
  public int getPagingSize() {
    return sharedPreferences.getInt(PropertyKey.PAGING_SIZE.getPropertyKey(), 0);
  }

  /**
   * @param propertyKey {@link SharedPreferences} key which maps to a {@code Set<String>}
   * @param value The string to be added to the {@code Set<String>} corresponding the key
   */
  private void addToSet(PropertyKey propertyKey, String value) throws InternalSyncException {
    ArrayList<String> arrayList = getArrayList(propertyKey);
    if (!arrayList.contains(value)) {
      arrayList.add(value);
      putArrayList(propertyKey, arrayList);
    }
  }

  /**.
   *
   * @param propertyKey {@link SharedPreferences} key which maps to {@code Set<String>}
   * @param value The string to be remove from the {@code Set<String>} corresponding the key
   */
  private void removeFromSet(PropertyKey propertyKey, String value)
      throws InternalSyncException {
    ArrayList<String> arrayList = getArrayList(propertyKey);
    arrayList.remove(value);
    putArrayList(propertyKey, arrayList);
  }

  /**
   * Put array list
   */
  private void putArrayList(PropertyKey propertyKey, ArrayList<String> arrayList)
      throws InternalSyncException {
    ObjectMapper mapper = new ObjectMapper();
    ObjectWriter writer = mapper.writerWithType(new TypeReference<ArrayList<String>>() {});
    String str;
    try {
      str = writer.writeValueAsString(arrayList);
    } catch (Exception e) {
      throw new InternalSyncException(e, ExceptionType.INTERNAL_EXCEPTION, e.getMessage(),
          Log.ERROR);
    }
    Editor editor = sharedPreferences.edit();
    editor.putString(propertyKey.getPropertyKey(), str);
    editor.commit();
  }

  /**
   * Get the ArrayList<String> for the property.
   *
   * @param propertyKey {@link SharedPreferences} key which maps to {@code Set<String>}
   * @return The {@code ArrayList<String>} corresponding to the key.
   */
  private ArrayList<String> getArrayList(PropertyKey propertyKey) throws InternalSyncException {
    String setAsString = sharedPreferences.getString(propertyKey.getPropertyKey(), null);
    if (setAsString == null) {
      return new ArrayList<String>();
    }
    ObjectMapper mapper = new ObjectMapper();
    ArrayList<String> arrayList;
    try {
      arrayList = mapper.readValue(setAsString, new TypeReference<ArrayList<String>>() {});
    } catch (Exception e) {
      throw new InternalSyncException(e, ExceptionType.INTERNAL_EXCEPTION, e.getMessage(),
          Log.ERROR);
    }
    return arrayList;
  }

  /**
   * @param context
   * @return  The default config the application uses.
   */
  public static Config getDefaultConfig(Context context) {
    if (defaultConfig == null) {
      SharedPreferences sharedPreferences =
          context.getSharedPreferences(SYNC_CONFIG_NAME, Context.MODE_PRIVATE);
      defaultConfig = new Config(context, sharedPreferences);
      if (defaultConfig.getPagingSize() <= 0) {
        defaultConfig.setPageSize(PAGE_SIZE);
      }
    }
    return defaultConfig;
  }

  /**
   * Check whether the config is sane.
   */
  public boolean isSane() {
    if (getUsername() == null) {
      SyncClientLog.logDebug(SyncableSharedPreferencesAdapter.class, USERNAME_NOT_SET);
      return false;
    }
    if (getPagingSize() <= 0) {
      SyncClientLog.logDebug(SyncableSharedPreferencesAdapter.class, PAGING_SIZE_LESS_THAN_ZERO);
      return false;
    }

    // It may happen that account got deleted by the user.
    return checkIfAccountExists(getUsername());
  }

  /**
   * Checks whether the account is a valid android account.
   */
  private boolean checkIfAccountExists(String username) {
    if (username == null) {
      return false;
    }
    Account[] accounts = AccountManager.get(context).getAccountsByType(ACCOUNT_TYPE);
    for (Account account : accounts) {
      if (account.name.equals(username)) {
        return true;
      }
    }
    SyncClientLog.logDebug(SyncableSharedPreferencesAdapter.class,
        String.format(USERNAME_DOES_NOT_EXISTS, username));
    return false;
  }

  /**
   * @param registrationId gcm registration id.
   */
  public void setGCMRegistrationId(String registrationId) {
    Editor editor = sharedPreferences.edit();
    editor.putString(PropertyKey.GCM_REGISTRATION_ID.getPropertyKey(), registrationId);
    editor.commit();
  }

  /**
   * @returns the stored gcm registration id. returns null if not stored.
   */
  public String getGCMRegistrationId() {
    return sharedPreferences.getString(PropertyKey.GCM_REGISTRATION_ID.getPropertyKey(), null);
  }

  /**
   * Set whether a GCM device id has been set in the server.
   *
   * @param sent whether device id is sent to the server.
   */
  public void setDeviceIdSent(boolean sent) {
    Editor editor = sharedPreferences.edit();
    editor.putBoolean(PropertyKey.GCM_ID_SENT.getPropertyKey(), sent);
    editor.commit();
  }

  /**
   * Get device id sent.
   */
  public boolean isDeviceIdSent() {
    return sharedPreferences.getBoolean(PropertyKey.GCM_ID_SENT.getPropertyKey(), false);
  }
}
